cfn-initの実行時に「Unhandled exception during build: ‘utf8’ codec can’t decode byte…」のエラーが出る件について
しばたです。
CloudFormationでWindows Server EC2の初期設定をcfn-initヘルパースクリプトを使って実装しようとした際にとあるエラーでハマったので、その内容と回避策について本記事で共有します。
前提条件
今回問題が起きた環境は日本語Windows Server 2019 EC2インスタンスでcfn-init
を実行する際に発生しています。
詳細は後述しますが、おそらく英語版Windows Serverでは発生せず非英語圏の言語版であれば他の言語でも発生しそうです。
Windowsのバージョンも関係なくどのバージョンでも発生すると思われます。
なおLinuxでは確認していないため同様の問題が発生するかは不明です。
何が起こったか?
cfn-init
を使ってEC2インスタンスの初期設定のコマンドを実行した際に、コマンドの内容自体は正しいにも関わらず、
Unhandled exception during build: 'utf8' codec can't decode byte 0xnn in position nn: invalid start byte
(nnの部分はコマンドの内容により変わる)
といったエラーが発生しcfn-init
が異常終了してしまう事象に遭遇しました。
cfn-init.log
には以下の様な感じで出力されます。
2020-10-31 08:36:30,915 [ERROR] -----------------------BUILD FAILED!------------------------
2020-10-31 08:36:30,915 [ERROR] Unhandled exception during build: 'utf8' codec can't decode byte 0x83 in position 244: invalid start byte
Traceback (most recent call last):
File "cfn-init", line 171, in <module>
File "cfnbootstrap\construction.pyc", line 129, in build
File "cfnbootstrap\construction.pyc", line 530, in build
File "cfnbootstrap\construction.pyc", line 544, in run_config
File "cfnbootstrap\construction.pyc", line 138, in run_commands
File "cfnbootstrap\command_tool.pyc", line 120, in apply
File "encodings\utf_8.pyc", line 16, in decode
UnicodeDecodeError: 'utf8' codec can't decode byte 0x83 in position 244: invalid start byte
発生頻度としてはコマンドが複雑な方が出やすいですが、内容が単純な場合でも発生する事があり、例えば以下の様なテンプレートでも再現させることができます。
AWSTemplateFormatVersion: 2010-09-09
Resources:
# ・・・省略・・・
# 最低限の部分のみ記載
SampleEC2:
Type: AWS::EC2::Instance
Metadata:
AWS::CloudFormation::Init:
configSets:
ascending:
- config1
config1:
files:
C:\cfn\do-something.ps1:
content:
Fn::Sub: |
Get-NetAdapter
commands:
0-do-something:
command: powershell.exe -ExecutionPolicy Bypass C:\cfn\do-something.ps1
waitAfterCompletion: 0
1-signal-resource:
command:
Fn::Sub: |
cfn-signal.exe -e %ERRORLEVEL% --resource SampleEC2 --stack ${AWS::StackName} --region ${AWS::Region}
CreationPolicy:
ResourceSignal:
Count: 1
Timeout: PT20M
Properties:
ImageId:
Ref: WindowsLatestAmi
UserData:
Fn::Base64:
Fn::Sub: |
<script>
cfn-init.exe -v -s ${AWS::StackId} -r SampleEC2 --configsets ascending --region ${AWS::Region}
</script>
# ・・・後略・・・
この内容ではC:\cfn\do-something.ps1
というスクリプトを配備しGet-NetAdapter
を実行する様にしているだけです。
通常実行すれば下図の様な結果を表示するだけですが、
実際には先述の通りのエラーとなり、cfn-init-cmd.log
の詳細ログは以下の通り半端な出力を返します。
2020-10-31 08:36:23,680 P4536 [INFO] ============================================================
2020-10-31 08:36:23,680 P4536 [INFO] Command 0-do-something
2020-10-31 08:36:30,915 P4536 [INFO] -----------------------Command Output-----------------------
2020-10-31 08:36:30,915 P4536 [INFO]
2020-10-31 08:36:30,915 P4536 [INFO] Name InterfaceDescription ifIndex Status MacAddress LinkSpeed
2020-10-31 08:36:30,915 P4536 [INFO] ---- -------------------- ------- ------ ---------- ---------
2020-10-31 08:36:30,915 P4536 [INFO]
2020-10-31 08:36:30,915 P4536 [INFO]
2020-10-31 08:36:30,915 P4536 [INFO] ------------------------------------------------------------
(7行目は本来ENIの情報が出力されるはずだが空になっている)
原因は?
根本原因は正直わかりません。
ただTwitterで頂いた情報[1]によるとコマンドの出力に日本語が混ざる場合に起きるようです。
実際先述のGet-NetAdapter
の場合は本来出力に「イーサネット 3」があり、ログからまさにその部分が欠落してしまっています。
具体的にどの様な文字種が混ざるとダメなのかまでは調査していませんが(というか無理)、エラー自体も明らかにエンコーディング周りで何か起きている風ですし、日本語全般がアウトと考えておいた方が良い気がしています。
また、こちらのブログによればcfn-init
の-v
オプションを除くことでログ出力を抑止して回避できたそうですが、私の場合は-v
オプションを除いても問題は解消しませんでした。
回避策
根本原因は分かりませんが「日本語が出力されるとアウト」というのが分かれば回避策は割と容易です。
回避策1. コマンドの出力を抑止する
日本語を出力させなければ良いので[void]キャスト
や> $null
を使い出力自体を抑止してやればエラーは起きません。
# 通常のコマンドだと出力があるのでアウト
Get-NetAdapter
# voidキャストで出力を破棄する
[void](Get-NetAdapter)
# $nullリダイレクトで出力を破棄する
Get-NetAdapter *> $null
単純な方法ですが効果はあります。
ただし、実行結果を破棄してしまうのはデメリットも多いのでこの方法を使えるケースはさほど多くないでしょう。
コマンド自体が割と単純であるである場合、実行結果を気にしないで良い場合に使えます。
回避策2. 出力をファイルリダイレクトする
もう一つの回避策はコマンド出力を破棄するのでは無くファイルリダイレクトさせてしまうものです。
cfn-init
は母体が組み込みPythonであり各コマンドの標準出力をキャプチャしています。
コマンドの結果をPython側に渡さずファイルリダイレクトでファイルに直接出力してしまえば、エラーを回避できる上にその内容も残ります。
# 通常のコマンドだと出力があるのでアウト
Get-NetAdapter
# ファイルリダイレクトで Python側への出力を回避
Get-NetAdapter *> C:\cfn\workaround.log
# コマンドのボリュームが多い場合は &{} の形式でスクリプトブロックでまとめてしまうと良い
&{
Get-NetAdapter
#
# do other many commands...
#
} *> C:\cfn\workaround.log
PowerShell文法に関する補足
ここでPowerShellに不慣れな方に向けて軽く補足を入れておきます。
まずファイルリダイレクトの*>
はPowerShell 3.0から利用可能な構文です。
これは他シェルの> 2>&1
[2]に近いものなのですが、PowerShellは他シェルと違い標準出力に相当するストリームというものがあり、ストリームは1>
や2>
以外に3> (Warning)
、4> (Verbose)
、5> (Debug)
、6> (Information)
まで存在します。
*>
は全てのストリームの出力結果をまとめてリダイレクトする構文となります。
ストリームの詳細については個人ブログに書いてますのでこちらをご覧ください。
また、&{}
については{}
で複数の文を囲う事でスクリプトブロックとして宣言することができ、これは匿名関数のようなものだと考えてもらうとわかりやすいかと思います。
&
は起動演算子と呼ばれ右オペランドにあるコマンドやスクリプトブロックを実行する演算子であり、&{}
と書くことで宣言したスクリプトブロックを実行することができます。
最後に
&{
#
# do many commands...
#
} *> C:\cfn\workaround.log
の様に書くことで「一連の処理を匿名関数化し直ちに実行、匿名関数の出力をリダイレクトさせる」様な動作をさせることができます。
パフォーマンス的には若干不利になるのですが多くの処理がある場合にカジュアルに処理をまとめてリダイレクトできるためオススメです。
エラーを回避できるテンプレート例
ここまでを踏まえてエラーを回避できるテンプレートはこちらになります。
前節で説明したとおり&{}
を使いコマンド全体をくくる様にしています。
AWSTemplateFormatVersion: 2010-09-09
Resources:
# ・・・省略・・・
# 最低限の部分のみ記載
SampleEC2:
Type: AWS::EC2::Instance
Metadata:
AWS::CloudFormation::Init:
configSets:
ascending:
- config1
config1:
files:
C:\cfn\do-something.ps1:
content:
Fn::Sub: |
# We need to privent to output NON-ASCII characters to stdout.
&{
Get-NetAdapter
} *> C:\cfn\do-something.log
commands:
0-do-something:
command: powershell.exe -ExecutionPolicy Bypass C:\cfn\do-something.ps1
waitAfterCompletion: 0
1-signal-resource:
command:
Fn::Sub: |
cfn-signal.exe -e %ERRORLEVEL% --resource SampleEC2 --stack ${AWS::StackName} --region ${AWS::Region}
CreationPolicy:
ResourceSignal:
Count: 1
Timeout: PT20M
Properties:
ImageId:
Ref: WindowsLatestAmi
UserData:
Fn::Base64:
Fn::Sub: |
<script>
cfn-init.exe -v -s ${AWS::StackId} -r SampleEC2 --configsets ascending --region ${AWS::Region}
</script>
# ・・・後略・・・
実行結果もこんな感じでC:\cfn\do-something.ps
とC:\cfn\do-something.log
が生成されています。
C:\cfn\do-something.log
の中身はこんな風に良い感じに出力されています。
Name InterfaceDescription ifIndex Status MacAddress LinkSpeed
---- -------------------- ------- ------ ---------- ---------
イーサネット 3 Amazon Elastic Network Adapter 8 Up 06-CC-09-48-5A-80 25 Gbps
もちろんcfn-init
自体も正常終了しており、cfn-init.log
の当該部分はこの様なエラー無く完了する様になりました。
2020-10-31 10:03:29,325 [DEBUG] Running command 0-do-something
2020-10-31 10:03:29,325 [DEBUG] No test for command 0-do-something
2020-10-31 10:03:34,796 [INFO] Command 0-do-something succeeded
2020-10-31 10:03:34,796 [DEBUG] Command 0-do-something output:
【2010.11.01追記】もう一つの回避策
最初にこの記事を公開した後でふと、
Python側がUTF-8を期待するなら [Console]::OutputEncoding を弄ってしまえばよいのでは?
と思い至り試してみました。
すなわち、スクリプトの先頭で[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
を仕込みコンソール出力のエンコーディングをUTF-8に強制する様にするわけです。
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
Get-NetAdapter
テンプレート全体だとこんな感じ。
AWSTemplateFormatVersion: 2010-09-09
Resources:
# ・・・省略・・・
# 最低限の部分のみ記載
SampleEC2:
Type: AWS::EC2::Instance
Metadata:
AWS::CloudFormation::Init:
configSets:
ascending:
- config1
config1:
files:
C:\cfn\do-something.ps1:
content:
Fn::Sub: |
# We need to specify [Console]::OutputEncoding to UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
Get-NetAdapter
commands:
0-do-something:
command: powershell.exe -ExecutionPolicy Bypass C:\cfn\do-something.ps1
waitAfterCompletion: 0
1-signal-resource:
command:
Fn::Sub: |
cfn-signal.exe -e %ERRORLEVEL% --resource SampleEC2 --stack ${AWS::StackName} --region ${AWS::Region}
CreationPolicy:
ResourceSignal:
Count: 1
Timeout: PT20M
Properties:
ImageId:
Ref: WindowsLatestAmi
UserData:
Fn::Base64:
Fn::Sub: |
<script>
cfn-init.exe -v -s ${AWS::StackId} -r SampleEC2 --configsets ascending --region ${AWS::Region}
</script>
# ・・・後略・・・
結果としてはこれでも期待した動作になりました。
cfn-init.log
はこんな感じです。
2020-10-31 16:22:25,588 [DEBUG] Running command 0-do-something
2020-10-31 16:22:25,588 [DEBUG] No test for command 0-do-something
2020-10-31 16:22:31,509 [INFO] Command 0-do-something succeeded
2020-10-31 16:22:31,509 [DEBUG] Command 0-do-something output:
Name InterfaceDescription ifIndex Status MacAddress LinkSpeed
---- -------------------- ------- ------ ---------- ---------
イーサネット 3 Amazon Elastic Network Adapter 8 Up 06-12-C3-20-30-C2 25 Gbps
全てのケースでこの回避策が使えるかまでは確認できませんが、まずはこちらを試し、駄目ならリダイレクトにすると良いかと思います。
最後に
以上となります。
初見では非常にわかりにくいエラーですが何が起きてるか分かってしまえば回避は容易です。
そもそも論としてWindows環境でcfn-init
を使う方は少ないかもしれませんが、同様のエラーで困っている人の役に立てば幸いです。